package Server;


import java.io.*;
import java.text.*;
import java.util.*;

/**
 * <p>Title: Response.java</p>
 *
 * <p>Description: Used to process incoming requests to the server and generate
 * the appropriate response to that request. This is where most of the server
 * processing is done, thus making it a very important class to the
 * implementation of your server.</p>
 *
 * @Author: YingLuan Tan, Yves Galang
 * @Course: CSC667
 * @Date: 2012 Spring
 */

public class Response {

    /**
     * Default constructor for the response object. Variables are reset and/or
     * intialized here. These variables will be used throughout request
     * processing and response generation.
     */
    public Response() {
        codes = new HashMap<String, String>();
        codes.put("200", "OK");
        codes.put("201", "Created");
        codes.put("204", "No Content");
        codes.put("302", "Found");
        codes.put("304", "Not Modified");
        codes.put("400", "Bad Request");
        codes.put("401", "Unauthorized");
        codes.put("403", "Forbidden");
        codes.put("404", "Not Found");
        codes.put("500", "Internal Server Error");
        codes.put("501", "Not Implemented");

    }

    /**
     * Used to process the request that came from the client to the server.
     *
     * @param request Request object that was generated and stores all the
     * 					request information that came in from the client
     * @param MIMETable Hashtable which contains the various mime types the
     * 					server can handle.
     * @param env Environment object that contains the environment variables
     * 				necessary for cgi script execution.
     * @param httpdConf HttpdConf object that has all the configuration
     */
    public void processRequest(Request request, HashMap<String, String> MIMETable, Environment env, HttpdConf httpdConf) 
    		throws Exception 
    {
        _request = request;
        _http_ver = _request.getHttp_ver();
        _method = _request.getMethod();
        _lastModifiedDate ="";
        _serverRoot = httpdConf.getSettings("ServerRoot");
        
        String realPath = httpdConf.solveAlias(_request.getRequest_uri());
        if(realPath == null)
        {
        	processForbidden();
        	_response_content_type = "text/html";
        	return;
        }
        _response_content_type = getContentType(realPath, MIMETable);
        

        _isScript = httpdConf.isScript(_request.getRequest_uri());
        if (_isScript) 
        	processScript(env, httpdConf.getAuthentication(), realPath);
        else 
            if (_method.equals("GET") || _method.equals("HEAD"))
            	process(realPath, httpdConf.getAuthentication());
            else if (_method.equals("POST")) {
            } else if (_method.equals("PUT")) {
                _request.getBody();
                FileOutputStream fw = new FileOutputStream(realPath);
                fw.write(_body);
                fw.flush();
                _response_code = "201";
            }
        
    }

    /**
     * processes normal web page requests
     * this function sets the response code, response type, and body
     * 
     * @param realPath			local file path
     * @param authentication	Authentication object
     */
    private void process(String realPath, Authentication authentication) 
    {
    	File inFile;
        if( checkAuthentication(realPath, _request , authentication))
        {
        	inFile = new File(realPath);
        	if( inFile.exists())
        		if(checkDate(inFile.lastModified()))
        		{
        			_response_code = "304";
        			_contentSize = 0;
        			_body =  new byte[0];
        			return;
        		}
        		else
        			_response_code = "200"; 
        	else
        	{
        		_response_code = "404";
        		inFile = new File(_serverRoot + "/error/404.html");
        	}
        }
		else
		{
        	_response_code = "401";
        	inFile = new File(_serverRoot + "/error/401.html");	
        }
             
        _body = readFile(inFile);
        
	}

	/**
	 * processes a forbidden request
	 */
	private void processForbidden() throws Exception 
    {
    	File inFile;
    	inFile = new File(_serverRoot + "/error/403.html");
    	_response_code = "403";
    	_contentSize = inFile.length();
    	_body = readFile(inFile);
    }
	
	
    /**
     * Process cgi requests
     *
	 * @param env
	 * @param authentication
	 * @param realPath		local file path to the script
     */	
	private void processScript(Environment env, Authentication authentication, String realPath)
	{
		try{
	        File inFile = new File(realPath);
	        if (inFile.exists())
	        	_response_code = "200";
	        else 
	        {
	        	_response_code = "404";
	        	_response_content_type = "text/html";
	        	_body = readFile( new File(_serverRoot + "/error/404.html"));
	        	return;
	        }
	
			if(checkAuthentication(realPath, _request , authentication))
			{
				CGIHandler cgi = new CGIHandler(_request, env);
				BufferedInputStream bis = cgi.runScript(realPath);
				while (bis.available() <= 0);
				_body = new byte[bis.available()];
				bis.read(_body);
				bis.close();
				_contentSize = _body.length;
			}
			else
			{
	        	_response_code = "401";
	        	_response_content_type = "text/html";
	        	_body = readFile( new File(_serverRoot + "/error/401.html"));	
			}
		}
		catch(Exception e)
		{
			System.err.print(e.getMessage() + "\r\n");
			_body = new byte[0];
			_contentSize = 0;
			_response_code = "500";
			_response_content_type = "text/html";
		}
	}
	
	
	/**
	 * reads a file into memory and store in an array of bytes
	 * 
	 * @param inFile	the file object to be read in
	 * @return	the file in byte[]
	 */
	private byte[] readFile(File inFile)
	{
		InputStream is;
		byte[] bytes = null;
		try{
			_contentSize = inFile.length();
			if(_method.equals("HEAD"))
				return new byte[0];

			is = new FileInputStream(inFile);
			bytes = new byte[(int)_contentSize];
			is.read(bytes);
			is.close();
		}
		catch(Exception e)
		{
			bytes = new byte[0];
			_contentSize = 0;
			_response_code = "500";
		}

		return bytes;

	}   
	
	
	
	
	/**
     * Used to output a correctly formatted response to the client. 
     * 
     * @param output OutputStream object that will be used to send the response
     * 					back to the socket.
     */
    public void writeOutput(OutputStream output) throws IOException 
    {
        BufferedOutputStream out = new BufferedOutputStream(output);
        PrintWriter writer = new PrintWriter(out, true);
        
		DateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss");  
		sdf.setTimeZone(TimeZone.getTimeZone("GMT"));  
		String now = sdf.format(new Date()) + " GMT";
		
		Calendar cal = Calendar.getInstance();
		cal.add(Calendar.DATE, 1);
		String future = sdf.format(cal.getTime()) + " GMT";
		
        StringBuffer tmp = new StringBuffer(512);
        
        tmp.append("HTTP/" + _http_ver + " " + _response_code + " " + codes.get(_response_code) + "\r\n");     
        tmp.append("Date: " + now + "\r\n");
        tmp.append("Server: " + "Test Server\r\n");
        if(_response_code.equals("401"))
        	tmp.append("WWW-Authenticate: Basic" + "\r\n");
        tmp.append("Cache-Control: max-age=3600" + "\r\n");
        tmp.append("Expires: " + future + "\r\n");
        if(_method.equals("GET") || _method.equals("HEAD"))
        	tmp.append("Last-Modified: " + _lastModifiedDate + "\r\n");
        tmp.append("Connection: close\r\n");
        if (_contentSize != 0) 
        {
        	tmp.append("Content-Length: " + _contentSize + "\r\n");
            if (!_isScript) 
            	tmp.append("Content-Type: " + _response_content_type + "\r\n\r\n");
        }
        
        writer.print(tmp);
        _logger = tmp.toString();
        writer.flush();         // flushing is important

        out.write(_body);
        out.flush();            // flush just to be sure

        writer.close();
    }

    
    
	/**
	 * handles exceptionally bad situations like a bad request or internal errors
	 * 
	 * @param errorStr
	 * @param output
	 */
	public void superBad(String errorStr, OutputStream output) 
	{
        BufferedOutputStream out = new BufferedOutputStream(output);
        PrintWriter writer = new PrintWriter(out, true);
        
		DateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss");  
		sdf.setTimeZone(TimeZone.getTimeZone("GMT"));  
		String now = sdf.format(new Date()) + " GMT";

        StringBuffer tmp = new StringBuffer(512);
        String response_code = errorStr.contentEquals("Internal Server Error")? "500": "400";
        
        tmp.append("HTTP/" + _http_ver + " " + response_code + " " + codes.get(response_code) + "\r\n");     
        tmp.append("Date: " + now + "\r\n");
        tmp.append("Server: " + "Test Server\r\n");
        tmp.append("Connection: close\r\n");
        if (_contentSize != 0) 
        {
        	tmp.append("Content-Length: " + "\r\n");
            if (!_isScript) 
            	tmp.append("Content-Type: " + "\r\n\r\n");
        }
        
        writer.print(tmp);
        _logger = tmp.toString();
        writer.flush();         // flushing is important
        writer.close();
		
	}
    
    
    /**
     * Used to test for authentication. 
     *
     * @param requestPath	the path to check for authentication
     * @param request 		the Request object
     * @param authen		the Authentication object
     * 
     * @return				TRUE if path is accessible by current client
     * 						FALSE if no accessble by current client
     */
    private boolean checkAuthentication(String requestPath, Request request, Authentication authen) 
    {
    	if(!authen.pathSecured(requestPath))
    		return true;
    	
    	String userInfo;
    	if(request.getHeader().containsKey("Authorization"))
    		userInfo = request.getHeader().get("Authorization");
    	else
    		return false;

    	userInfo = userInfo.substring(6);
    	return authen.checkAuth(userInfo);   		
    }

    
    
    
    /**
     * finds the local file's mime type
     * 
     * @param path		the local file path
     * @param _mimeType	the HashMap that contains all mime types
     * @return	the mime type of the file
     */
    private String getContentType(String path, HashMap<String, String> _mimeType) 
    {
        String fileExt = path.substring(path.lastIndexOf(".") + 1);
        String tmp = (String) _mimeType.get(fileExt);
        if (fileExt.equals("pl") || fileExt.equals("py")) 
            tmp = "text/html";
        
        return tmp == null ? "application/unknown" : tmp;
    }

 
    /**
     * Used to write the appropriate information to the log file.
     *
     * @param logPath String value which contains the location of your log file
     * @param host String value that contains the address of the client who made
     * the request
     */
    public synchronized void writeToLog(String logPath, String host, String addr) {
        try {
            FileWriter fw = new FileWriter(logPath, true);
            BufferedWriter bw = new BufferedWriter(fw);
            String log = "";
            log += host + " " + addr + " ";
            log += (new Date()).toString() + " ";
            log += _request.getMethod() + " " + _request.getRequest_uri() + _request.getQuery() + " HTTP/" + _request.getHttp_ver() + "\n";
            bw.write(log);
            bw.close();
        } catch (IOException ex) {
            System.out.println("Error Opening File");
            System.exit(1);
        }
    }
    
    
    

	/**
	 * handles last modified date (aka caching)
	 * 
	 * @param fileDate		the requested file's last modified date
	 * @return				true if fileDate is NOT later than client submitted date (304)
	 * 						false if NO client submitted date OR fileDate IS later than client submitted date
	 */
	private boolean checkDate(long fileDate)
	{
		try{
			DateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss");  
			sdf.setTimeZone(TimeZone.getTimeZone("GMT"));  

			Date lastDate = new Date(fileDate);
			_lastModifiedDate = sdf.format(lastDate);
			lastDate = sdf.parse(_lastModifiedDate);
			_lastModifiedDate = sdf.format(lastDate) + " GMT";

			HashMap<String, String> headers = _request.getHeader();

			if(headers.containsKey("If-Modified-Since"))
			{
				String sinceDateStr = (String) headers.get("If-Modified-Since");
				sinceDateStr = sinceDateStr.substring(0, sinceDateStr.length()-4);

				Date sinceDate = null;

				sinceDate = sdf.parse(sinceDateStr);
				if(lastDate.compareTo(sinceDate) <= 0)
					return true;
			}
		}
		catch(ParseException e)
		{	System.err.print(e.getMessage());	}

		return false;
	}
	
	
	
	/**
	 * prints this response to console
	 */
	public void print() 
	{	System.out.print(_logger);	}
    
    
    private HashMap<String, String> codes;
    private String _http_ver;
    private String _response_code;
    private String _response_content_type;
    private boolean _isScript;
    private byte[] _body;
    private long _contentSize;
    private String _method;
    private Request _request;
    private String _lastModifiedDate;
    private String _logger;
    private String _serverRoot;
    
    
}

